<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>KonvaJS text paths</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=1.0, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        margin: 0;
        width: 100vw;
        height: 100vh;
      }
      aside {
        margin-left: 20px;
      }
      div {
        padding: 2px;
      }
    </style>
  </head>

  <body>
    <aside>
      <div>
        <label>
          <span>Curvature</span>
          <input type="range" value="0" id="radius" min="-100" max="100" />
          <input type="number" value="0" id="curvature" />
        </label>
      </div>
      <div>
        <label>
          <span>Font size</span>
          <input type="range" value="0" id="fontsize" min="1" max="100" />
        </label>
      </div>
      <div>
        <label>
          <span>Alignment</span>
          <select id="align">
            <option value="center">center</option>
            <option value="left">left</option>
            <option value="right">right</option>
          </select></label
        >
      </div>
      <div>
        <label>
          <span>Font weight</span>
          <select id="fontweight">
            <option value="normal">normal</option>
            <option value="bold">bold</option>
            <option value="italic">italic</option>
          </select></label
        >
      </div>
      <div>
        <label>
          <span>Text decoration</span>
          <select id="textdecoration">
            <option value="">none</option>
            <option value="line-through">line-through</option>
            <option value="underline">underline</option>
          </select></label
        >
      </div>
      <div>
        <label>
          <span>Text</span>
          <textarea id="textinput">Curved text</textarea>
        </label>
      </div>
    </aside>

    <div id="container"></div>

    <script type="module">
      import Konva from '../src/index.ts';

      const stage = new Konva.Stage({
        container: 'container',
        width: window.innerWidth,
        height: window.innerHeight,
      });

      const layer = new Konva.Layer();
      stage.add(layer);

      // define variables
      let alignXShift = 0;
      let alignYShift = 0;
      let curvature = 0;
      let helpersTimeout;

      // define constants
      const RADIUS = 50;
      let shiftX = 400;
      let shiftY = 200;
      const RANGE = 100000;

      const getRadius = (currentValue) =>
        Math.abs(1 / Math.tan(currentValue / 100)) * RADIUS;

      const isOutOfRange = () => validate(getRadius(curvature)) === RANGE;
      const getArcSweep = () => (Math.sign(curvature) >= 0 ? 0 : 1);

      const validate = (number) => {
        return Math.max(-RANGE, Math.min(number, RANGE));
      };

      // define arc calculation
      // Credits to @opsb https://stackoverflow.com/a/18473154 for the polarToCartesian and describeArc functions
      const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => ({
        x: validate(
          centerX + radius * Math.cos(((angleInDegrees - 90) * Math.PI) / 180)
        ),
        y: validate(
          centerY + radius * Math.sin(((angleInDegrees - 90) * Math.PI) / 180)
        ),
      });
      const describeArc = (x, y, radius, startAngle, endAngle, sweep) => {
        const validatedRadius = validate(radius);

        if (isOutOfRange()) {
          const width = text.getTextWidth();

          return {
            data: `M ${-width / 2} 0 L ${width / 2} 0`,
            start: { x, y },
          };
        }

        const endAngleOriginal = endAngle;
        if (endAngleOriginal - startAngle === 360) {
          endAngle = 359;
        }
        const start = polarToCartesian(x, y, validatedRadius, endAngle);
        const end = polarToCartesian(x, y, validatedRadius, startAngle);

        return {
          data: [
            'M',
            start.x,
            start.y,
            'A',
            validatedRadius,
            validatedRadius,
            1,
            1,
            sweep,
            end.x,
            end.y,
            endAngleOriginal - startAngle === 360 ? 'z' : '',
          ].join(' '),
          start,
          radius: validatedRadius,
          deltaAngle: endAngle - startAngle,
        };
      };

      // create elements
      const text = new Konva.TextPath({
        text: 'Curved text',
        align: 'center',
        data: 'M 0 0',
        fontSize: 20,
        textBaseline: 'middle',
        fill: 'black',
      });
      const group = new Konva.Group({ draggable: true });

      // helpers
      const transformer = new Konva.Transformer({
        resizeEnabled: true,
        rotateEnabled: true,
        shouldOverdrawWholeArea: true,
      });
      const positioner = new Konva.Rect({
        fill: 'green',
        opacity: 0.1,
        visible: false,
      });
      const circleCenter = new Konva.Rect({
        fill: 'red',
        opacity: 0.1,
        visible: false,
      });
      const path = new Konva.Path({
        stroke: 'black',
        opacity: 0.3,
        visible: false,
      });

      const getArcData = () =>
        describeArc(0, 0, getRadius(curvature), 0, 360, getArcSweep());

      // create methods to calculate positions
      const calculateTextPlacement = () => {
        const { data, start } = getArcData();
        const x = shiftX + start.x - alignXShift;
        const y = shiftY + start.y - alignYShift;

        text.data(data);
        text.x(x);
        text.y(y);
      };
      const calculatePositionerPlacement = () => {
        const { start } = getArcData();

        positioner.x(
          text.x() - start.x - text.getTextWidth() / 2 + alignXShift
        );
        positioner.y(
          text.y() - start.y - text.getTextHeight() / 2 + alignYShift
        );
        positioner.width(text.getTextWidth());
        positioner.height(text.getTextHeight());
      };
      const calculateCircleCenterPlacement = () => {
        const { start } = getArcData();

        let centerShiftY = 0;
        if (getArcSweep() === 1) {
          const alignment = text.align();
          if (alignment !== 'center') {
            centerShiftY = -start.y * 2;
          } else {
            centerShiftY = start.y * 2;
          }
        }

        const x = shiftX + start.x - alignXShift;
        const y = shiftY + start.y - alignYShift + centerShiftY;

        circleCenter.x(x - 2);
        circleCenter.y(y - 2);
        circleCenter.width(5);
        circleCenter.height(5);
      };
      const calculatePathPlacement = () => {
        const { data, start } = getArcData();
        const x = shiftX + start.x - alignXShift;
        const y = shiftY + start.y - alignYShift;

        path.data(data);

        path.x(x);
        path.y(y);
      };

      // calculate initial positions
      calculateTextPlacement();
      calculatePathPlacement();
      calculatePositionerPlacement();

      // update positions on change
      const setPosition = () => {
        calculateTextPlacement();
        calculatePathPlacement();
        calculatePositionerPlacement();
        calculateCircleCenterPlacement();
      };

      const updateHelpersVisibility = (showHelpers) => {
        if (helpersTimeout) {
          clearTimeout(helpersTimeout);
        }

        // force transformer update
        if (transformer.nodes().length > 0) {
          transformer.nodes([group]);
        }

        helpersTimeout = setTimeout(() => {
          updateHelpersVisibility(false);
        }, 2000);

        positioner.visible(showHelpers);
        circleCenter.visible(showHelpers);
        path.visible(showHelpers);
      };

      // create methods to correct rotation and alignment
      const correctRotation = () => {
        const value = text.align();
        if (isOutOfRange()) {
          text.rotation(0);
          path.rotation(0);
        } else if (value === 'right') {
          text.rotation(180);
          path.rotation(180);
        } else if (value === 'left') {
          text.rotation(180);
          path.rotation(180);
        } else {
          text.rotation(0);
          path.rotation(0);
        }
      };
      const correctAlignment = () => {
        const { start, radius, deltaAngle } = getArcData();
        const value = text.align();
        if (isOutOfRange()) {
          alignXShift = 0;
        } else if (value === 'right') {
          alignXShift = -text.getTextWidth() / 2;
        } else if (value === 'left') {
          alignXShift = text.getTextWidth() / 2;
        } else {
          alignXShift = 0;
        }

        if (value === 'center' && getArcSweep() === 1) {
          alignYShift = start.y * 4;
          alignXShift = start.x * 2;
        } else {
          alignYShift = 0;
        }
      };

      // attach handlers
      document
        .querySelector('#align')
        .addEventListener('change', ({ target: { value } }) => {
          text.align(value);
          correctAlignment();
          correctRotation();
          setPosition();
          updateHelpersVisibility(false);
        });
      document
        .querySelector('#fontweight')
        .addEventListener('change', ({ target: { value } }) => {
          text.fontStyle(value);
          updateHelpersVisibility(false);
        });
      document
        .querySelector('#textinput')

        .addEventListener('input', ({ target: { value } }) => {
          text.text(value);
          setPosition();
          updateHelpersVisibility(false);
        });
      document
        .querySelector('#textdecoration')
        .addEventListener('change', ({ target: { value } }) => {
          text.textDecoration(value);
          updateHelpersVisibility(false);
        });
      document
        .querySelector('#fontsize')
        .addEventListener('input', ({ target: { value } }) => {
          text.fontSize(Number(value));
          setPosition();
          updateHelpersVisibility(false);
        });
      document
        .querySelector('#radius')
        .addEventListener('input', ({ target: { value } }) => {
          curvature = value;
          correctAlignment();
          correctRotation();
          setPosition();
          transformer.nodes([]);

          updateHelpersVisibility(true);

          document.querySelector('#curvature').value = value;
        });
      document
        .querySelector('#curvature')
        .addEventListener('input', ({ target: { value } }) => {
          curvature = value;
          correctAlignment();
          correctRotation();
          setPosition();
          updateHelpersVisibility(true);
          transformer.nodes([]);

          document.querySelector('#radius').value = value;
        });

      // attach handlers to konva elements
      group.on('click', (e) => {
        updateHelpersVisibility(false);
        transformer.nodes([group]);
      });
      group.on('dragmove', (e) => {
        updateHelpersVisibility(false);
      });

      group.on('transform', (e) => {
        updateHelpersVisibility(false);
      });
      stage.on('click', (e) => {
        if (e.target === stage) {
          transformer.nodes([]);
        }

        updateHelpersVisibility(false);
      });

      // keep curved text in a group
      group.add(text);
      group.add(path);
      // group.add(positioner);
      group.add(circleCenter);

      // add the shapes to the layer
      layer.add(group);

      layer.add(transformer);
    </script>
  </body>
</html>
